package com.woniu.utils;

import cn.dev33.satoken.session.SaSession;
import cn.dev33.satoken.stp.StpUtil;
import cn.dev33.satoken.util.SaResult;
import jakarta.annotation.Resource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;

/**
 * @Author pml
 * @Date 2025/6/14 10:41
 * Token工具类（WebFlux+Redis持久化版）
 * 为什么要用Mono？保证所有操作都在响应式上下文中，避免上下文丢失导致token失效
 */
@Component
public class TokenUtils {
    // 注入Redis模板，为什么用StringRedisTemplate？兼容性好，适合简单KV操作
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    //都没用上，yml设置了刷新令牌有效期,
    // 刷新令牌有效期（秒），与yml配置一致，7天
    private static final long REFRESH_TOKEN_TIMEOUT = 3 * 60;
//            7 * 24 * 60 * 60;
    // 访问令牌有效期（秒），与yml配置一致，30分钟
    private static final long ACCESS_TOKEN_TIMEOUT = 30 * 60;

    /**
     * 生成双令牌（访问令牌+刷新令牌）
     * 为什么增加refreshTokenTimeout参数？
     * 1. 支持动态设置过期时间，满足"记住我"功能需求
     * 2. 保持方法灵活性，可适应不同场景（如管理员登录时效更短）
     * @param userId 用户ID
     * @param refreshTokenTimeout 刷新令牌过期时间（秒）
     * @return 包含双令牌的Map
     */
    public Mono<Map<String, String>> generateDoubleToken(Integer userId, long refreshTokenTimeout) {
        return Mono.deferContextual(ctx -> {
            // 生成访问令牌（使用Sa-Token内置方法，自动关联用户会话）
            String accessToken = StpUtil.createLoginSession(userId);

            // 生成刷新令牌（UUID无连字符格式，减少存储体积）
            String refreshToken = UUID.randomUUID().toString().replace("-", "");

            // 存储刷新令牌到Redis（使用动态过期时间）
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            String refreshKey = "refreshToken:" + refreshToken;
            // 为什么显式设置过期时间？
            // 1. 确保令牌自动失效，避免内存泄漏
            // 2. 区分普通登录和"记住我"登录的安全策略
            ops.set(refreshKey, userId.toString(), refreshTokenTimeout, TimeUnit.SECONDS);

            // 封装返回结果（保持与前端约定的字段名）
            Map<String, String> tokenMap = new HashMap<>();
            tokenMap.put("accessToken", accessToken);
            tokenMap.put("refreshToken", refreshToken);
            return Mono.just(tokenMap);
        });
    }

    /**
     * 刷新访问令牌，校验refreshToken是否有效（Redis持久化）
     * 为什么用Mono.deferContextual？保证redis和token操作都在响应式上下文
     */
    public Mono<SaResult> refreshAccessToken(String refreshToken) {
        return Mono.deferContextual(ctx -> {
            ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();
            String refreshKey = "refreshToken:" + refreshToken;
            String userIdStr = ops.get(refreshKey);
            if (userIdStr == null) {
                // 刷新令牌无效或已过期，防止暴力破解
                return Mono.just(SaResult.error("刷新令牌无效或已过期"));
            }
            Integer userId = Integer.valueOf(userIdStr);
            // 生成新的访问令牌，避免频繁更换refreshToken导致体验差
            String newAccessToken = StpUtil.createLoginSession(userId);
            Map<String, String> tokenMap = new HashMap<>();
            tokenMap.put("accessToken", newAccessToken);
            tokenMap.put("refreshToken", refreshToken);
            return Mono.just(SaResult.ok().setMsg("令牌刷新成功").setData(tokenMap));
        });
    }

    /**
     * 校验访问令牌（可选，通常由Sa-Token拦截器自动完成）
     * 为什么用Mono？便于响应式链路手动校验
     */
    public Mono<Boolean> checkAccessToken(String accessToken) {
        return Mono.deferContextual(ctx -> {
            try {
                return Mono.just(StpUtil.getLoginIdByToken(accessToken) != null);
            } catch (Exception e) {
                return Mono.just(false);
            }
        });
    }

    /**
     * 使刷新令牌失效（登出时调用），Redis删除
     * 为什么用Mono？保证操作在响应式链路
     */
    public Mono<Void> invalidateRefreshToken(String refreshToken) {
        return Mono.deferContextual(ctx -> {
            String refreshKey = "refreshToken:" + refreshToken;
            stringRedisTemplate.delete(refreshKey);
            return Mono.empty();
        });
    }
}